/**
 * 191214：从旧库抽离
 * 200909：改进延迟执行函数waitToExecuteFrame，waitToExecuteTime，及cancelExecuteDelay
 * 200924：增加screenPointToWorld的输出参数
 * 201012：更正200909未顾及首执行和尾执行的错误
 * 201014：扩展clamp方法，区间上下限位置可随意
 * 201202：新增select方法
 * 210208：新增线性对照插值valueLinearMapping，未严格测试
 * 210510：新增setCaster中event的空值判断，以便直接使用输入pointer
 * 210910：新增创建和终止协程对象的方法
 * 220121：增加isGeneralArray方法
 * 20220304:[require] to [import]...jordia
 * 20220417:[instanceof Vector2 -> .isVector2]
 * 20220627:add function [decimalToString]
 */
 import {
    Vector2,Vector3,Raycaster,Matrix4,Plane,Vector4,Matrix3,Color,Quaternion,Euler,Skeleton,
    Scene,LoopRepeat,MathUtils,AnimationMixer,Audio, Object3D, Group, AnimationClip, SkinnedMesh,
     Bone, AnimationAction, AudioListener, Camera, AnimationActionLoopStyles
} from 'three';
import { CrossEventObject } from 'three-lib/container/dom-cross-platform-event';
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';

const D2R=Math.PI/180;
const MIN_FLOAT=1e-12;
const COMPARE_VALUE_DECREASE=(a:number,b:number):boolean=>{
    return a<b;
};
const COMPARE_EQUAL_DEFAULT=(a:number|string,b:number|string):boolean=>{
    return a===b;
};
const SORT_ARRAY=<T>(ary:Array<T>, compareFunc:CompareFunction<T>, lo:number, hi:number)=> {
    let i = lo, j = hi;
    let x = ary[Math.ceil((lo + hi) / 2)];
    do {
        while (compareFunc(ary[i], x))
            i++;
        while (compareFunc(x, ary[j]))
            j--;
        if (i <= j) {
            let tmp = ary[i];
            ary[i] = ary[j];
            ary[j] = tmp;
            i++;
            j--;
        }
    } while (i <= j);
    if (lo < j)
        SORT_ARRAY(ary, compareFunc, lo, j);
    if (i < hi)
        SORT_ARRAY(ary, compareFunc, i, hi);
};
interface CompareFunction<T>{
    (a:T,b:T):boolean
}
interface Loose<T>{
    [key:string]:T
}
interface AssetModel{
    animations: Array<AnimationClip>,
    scene: Group,
}
interface Timer{
    id?:number|null,
    time?:number,
    now?:number,
    counter?:number,
    info?:any,
    func?:{(t:number):boolean}
}
interface GetChildren<T>{
    (object:Array<T>|any):T
}
interface AnimateObject{
    uuid:string,
    root:Object3D,
    mixer:AnimationMixer,
    action:AnimationAction,
    length:number,
    prg:number,setPrg:{(prg:number):void}
}
interface AudioObject{
    content:Audio,
    mute:boolean,
    volume:number,
    active:{(bool:boolean):void},
    play:{(volumn:number):void},
    stop:{():void}
}
interface IntervalHandle{
    (interval:number):boolean
}
interface SequenceStep{
    [0]:AnimateObject,
    [1]:number,
    [2]:number,
    [3]:number,
    [4]:{():number},
    [5]:{(prg:number):void},
    [6]:{():void}
}
type TypedArray= Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array| Float64Array;
interface BaseRect{
    left:number,top:number,width:number,height:number,[key:string]:number
}
type ArrayLike<T>= TypedArray | Array<T>;
class BaseType{
    public static readonly NULL='null';
    public static readonly UNDEFINED= 'undefined';
    public static readonly NUMBER= 'number';
    public static readonly STRING= 'string';
    public static readonly BOOLEAN= 'boolean';
    public static readonly SYMBOL= 'symbol';
    public static readonly ARRAY= 'array';
    public static readonly VECTOR3= 'vector3';
    public static readonly VECTOR4= 'vector4';
    public static readonly VECTOR2= 'vector2';
    public static readonly MATRIX33= 'matrix3x3';
    public static readonly MATRIX44= 'matrix4x4';
    public static readonly COLOR= 'color';
    public static readonly QUATERNION= 'quaternion';
    public static readonly EULER= 'euler';
    public static readonly FUNCTION= 'function';
    public static readonly OBJECT= 'object';
}

const tV0=new Vector2();
const tV1=new Vector3();
const tV2=new Vector3();
const tV3=new Vector3();
const tR0=new Raycaster();
const tM0=new Matrix4();
const tP0=new Plane();

/**
 * 常用方法
 */
class Utils {
    constructor() {
        console.warn('do not instantiate class Utils,use the static method provided!');
    }

    /**
     * generate a new value, which is well-distributed with the given numbers
     * [2,5,4,9]->[2,4,5,9]->result [2,4,5,7,9]
     *      c,n,dta,if          s,e
     *      2,4,2,  true   ->   2,4
     *      4,5,1,  false  
     *      5,9,4,  true   ->   5,9
     * return (5+9)*0.5=7
     */
    public static averageDivide(nodes:Array<number>){
        let ta=[...nodes];
        Utils.quickSort(ta,COMPARE_VALUE_DECREASE);
        let s=0,e=0;
        let dta=0;
        for(let i=0;i<ta.length-1;i++){
            let c=ta[i],n=ta[i+1];
            let d=n-c;
            if(d>=dta){
                dta=d;
                s=c;
                e=n;
            }
        }
        return (s+e)*0.5;
    }

    /**
     * get the byte length of the given string
     */
    public static byteLengthOfString(str:string):number {
        let byteLength = 0;
        if (str === undefined || str.length === 0)return 0;
        for (let i = 0; i < str.length; i++) {
            if (str.charCodeAt(i) > 255) {
                byteLength += 2;
            } else {
                byteLength++;
            }
        }
        return byteLength;
    }

    /**
     * clamp a number by the limits, no order requirement
     */
    public static clamp(val:number,a:number,b:number):number{
        return a<b?Math.max(Math.min(val,b),a):Math.max(Math.min(val,a),b);
    }

    /**
     * (Common type or any type of object that implements. clone()) Implicit copy/clone
     *  (explicit method should be used when frequent copy is required)
     */
    public static copy<T>(source:any,type?:string):T|any {
        const _tp=type||Utils.typeOf(source);
        switch (_tp) {
            case BaseType.NUMBER:
                return source;
            case BaseType.STRING:
                return source;
            case BaseType.BOOLEAN:
                return source;
            case BaseType.SYMBOL:
                return source;
            case BaseType.UNDEFINED:
                return source;
            case BaseType.NULL:
                return source;
            case BaseType.VECTOR2:
                return new Vector2().copy(source);
            case BaseType.VECTOR3:
                return new Vector3().copy(source);
            case BaseType.VECTOR4:
                return new Vector4().copy(source);
            case BaseType.MATRIX33:
                return new Matrix3().copy(source);
            case BaseType.MATRIX44:
                return new Matrix4().copy(source);
            case BaseType.COLOR:
                return new Color().copy(source);
            case BaseType.QUATERNION:
                return new Quaternion().copy(source);
            case BaseType.EULER:
                return new Euler().copy(source);
            case BaseType.ARRAY:
                let tp = [];
                for (let i = 0; i < source.length; i++) {
                    tp[i] = Utils.copy(source[i]);
                }
                return tp;
            default:
                try {
                    if (Utils.typeOf(source.clone) === BaseType.FUNCTION) {
                        return source.clone();
                    } else {
                        let tp:Loose<any> = {};
                        for (let key in source) {
                            tp['' + key + ''] = Utils.copy(source['' + key + '']);
                        }
                        return tp;
                    }
                } catch (err) {
                    console.log('cannot copy the value of this source!',err);
                    return source;
                }
        }
    }

    /**
     * clone gltf file
     */
    public static cloneGLTF(gltf:GLTF):AssetModel{
        const clone = {
            animations: gltf.animations,
            scene: gltf.scene.clone(true)
        };
        const skinnedMeshes:Loose<SkinnedMesh> = {};
        gltf.scene.traverse((node:Loose<any>) => {
            if (node.isSkinnedMesh) {
                skinnedMeshes[node.name] = node as SkinnedMesh;
            }
        });
        const cloneBones:Loose<Bone> = {};
        const cloneSkinnedMeshes:Loose<SkinnedMesh> = {};
        clone.scene.traverse((node:Loose<any>) => {
            if (node.isBone) {
                cloneBones[node.name] = node as Bone;
            }
            if (node.isSkinnedMesh) {
                cloneSkinnedMeshes[node.name] = node as SkinnedMesh;
            }
        });
        for (let name in skinnedMeshes) {
            let skinnedMesh = skinnedMeshes[''+name+''];
            let skeleton = skinnedMesh.skeleton;
            let cloneSkinnedMesh = cloneSkinnedMeshes[''+name+''];
            let orderedCloneBones = [];
            for (let i = 0; i < skeleton.bones.length; ++i) {
                let cloneBone = cloneBones[skeleton.bones[i].name];
                orderedCloneBones.push(cloneBone);
            }
            cloneSkinnedMesh.bind(
                new Skeleton(orderedCloneBones, skeleton.boneInverses),
                cloneSkinnedMesh.matrixWorld);
        }
        return clone;
    }

    /**
     * Terminate the coroutine of timer object
     */
    public static cancelAnimationFrame(timer:Timer|number):void{
        if(timer===null||timer===undefined)return;
        const tp=Utils.typeOf(timer);
        if(tp==='number'){
            cancelAnimationFrame(timer as number);
            return;
        }
        if(!timer.hasOwnProperty('id'))return;
        cancelAnimationFrame((timer as Timer).id!);
        delete (timer as any).id;
    }

    /**
     * Get all elements of nested array/child
     */
    public static decomposeArray<T>(array:any,out:Array<T>,getChild:GetChildren<any>|null=null):void{
        out.length=0;
        let dps=(array:any,targetArray:Array<T>)=>{
            let ary=getChild?getChild(array):array;
            if(!ary){
                targetArray.push(array);
                return;
            }
            for(let i=0;i<ary.length;i++){
                let tp=getChild?getChild(ary[i]):ary[i];
                if(!Array.isArray(tp)){
                    targetArray.push(ary[i]);
                }else{
                    dps(ary[i],targetArray);
                }
            }
        };
        dps(array,out);
    }

    /**
     * generate average divided locations 
     * count=5,min=0.5,max=2,startDelta=0.5
     * count sps,                   dta, k,i,t              if                           sps
     * 1     [2],                   0.5, 1,1,2-0.5=1.5      1.5-0.5=1>min     ->false->  [2,1.5],k=2
     * 2     [2,1.5],               0.5, 2,2,1.5-0.5=1      1-0.5==0.5        ->true ->  [2,1.5,1],dta=0.25,k=0+1
     * 3     [2,1.5,1],             0.25,1,3,2-0.25=1.75    1.75-0.25=1.5>min ->false->  [2,1.5,1,1.75],k=2
     * 4     [2,1.75,1.5,1],        0.25,2,4,1.5-0.25=1.25  1.25-0.25=1>min   ->false->  [2,1.5,1,1.75,1.25],k=3
     * 5     [2,1.5,1,1.75,1.25]    0.25,3,5,1-0.25=0.75    0.75-0.25==0.5    ->true ->  [2,1.5,1,1.75,1.25,0.75],dta=0.125,k=0+1
     * return 0.75
     */
    public static divideLocation(count:number,out:Array<number>|null=null,min:number=0.5,max:number=2,startDelta:number=0.5):number{
        let sps = [max];
        let dta = startDelta;
        let k=1;
        for (let i = 1; i < count+1; i++) {
            let t=sps[k-1]-dta;
            if(t-dta<=min){
                dta/=2;
                k=0;
                sps.push(t);
            }else{
                sps.push(t);
            }
            k++;
        }
        if(out)out.push(...sps);
        return sps[sps.length-1];
    }

    /**
     * Convert decimal digits to characters
     */
    public static decimalToString(n:number,base:number=26,refCode:number=64):string{
        let str='';
        while (n>0){
            let m=n%base;
            if(m===0)m=base;
            str=String.fromCodePoint(m+refCode)+str;
            n=(n-m)/base;
        }
        return str;
    }

    /**
     * Constructing Euler angle in Y direction from Euler angle parameter in Z direction
     */
    public static eulerY(x:number,y:number,z:number,order:string='XYZ',isDegree:boolean=true):Euler{
        const fac=isDegree?D2R:1;
        let _x=x,_y=z,_z=-y,_o=order;
        switch (order) {
            case 'XYZ':
                _o='XZY';
                break;
            case 'XZY':
                _o='XYZ';
                break;
            case 'YXZ':
                _o='ZXY';
                break;
            case 'YZX':
                _o='XZY';
                break;
            case 'ZXY':
                _o='YXZ';
                break;
            case 'ZYX':
                _o='YZX';
                break;
            case 'xyz':
                _o='XZY';
                break;
            case 'xzy':
                _o='XYZ';
                break;
            case 'yxz':
                _o='ZXY';
                break;
            case 'yzx':
                _o='XZY';
                break;
            case 'zxy':
                _o='YXZ';
                break;
            case 'zyx':
                _o='YZX';
                break;
        }
        return new Euler(_x*fac,_y*fac,_z*fac,_o);
    }

    /**
     * (Common type or any type of object that implements. equals (other)) 
     * Implicit equality judgment (when frequent calculation is required, 
     * the type should be specified or explicit equality judgment should be used)
     */
    public static equal(a:any, b:any, type:boolean|string = false,precision:number=0):boolean {
        let _type = type;
        if (_type === false) {
            const type0 = Utils.typeOf(a);
            const type1 = Utils.typeOf(b);
            if (type0 !== type1) return false;
            _type = type1;
        }
        switch (_type) {
            case BaseType.NUMBER:
                if(!precision) return a === b;
                return Utils.zoneEqual(a,precision,b);
            case BaseType.STRING:
                return a === b;
            case BaseType.BOOLEAN:
                return a === b;
            case BaseType.UNDEFINED:
                return true;
            case BaseType.NULL:
                return true;
            case BaseType.MATRIX33:
                for (let i = 0; i < a.elements.length; i++) {
                    if(!precision) {
                        if (a.elements[i] !== b.elements[i]) return false;
                    }else{
                        if(!Utils.zoneEqual(a.elements[i],precision,b.elements[i]))return false;
                    }
                }
                return true;
            case BaseType.MATRIX44:
                for (let i = 0; i < a.elements.length; i++) {
                    if(!precision) {
                        if (a.elements[i] !== b.elements[i]) return false;
                    }else{
                        if(!Utils.zoneEqual(a.elements[i],precision,b.elements[i]))return false;
                    }
                }
                return true;
            case BaseType.VECTOR3:
                if(!precision) {
                    return a.x === b.x && a.y === b.y && a.z === b.z;
                }
                return Utils.zoneEqual(a.x ,precision, b.x) &&
                    Utils.zoneEqual(a.y ,precision, b.y) &&
                    Utils.zoneEqual(a.z ,precision, b.z);
            case BaseType.VECTOR2:
                if(!precision) {
                    return a.x === b.x && a.y === b.y;
                }
                return Utils.zoneEqual(a.x ,precision, b.x) &&
                    Utils.zoneEqual(a.y ,precision, b.y);
            case BaseType.VECTOR4:
                if(!precision) {
                    return a.x === b.x && a.y === b.y && a.z === b.z&&a.w===b.w;
                }
                return Utils.zoneEqual(a.x ,precision, b.x) &&
                    Utils.zoneEqual(a.y ,precision, b.y) &&
                    Utils.zoneEqual(a.z ,precision, b.z)&&
                    Utils.zoneEqual(a.w,precision,b.w);
            case BaseType.SYMBOL:
                return false;
            case BaseType.ARRAY:
                if (a.length !== b.length) return false;
                for (let i = 0; i < a.length; i++) {
                    if (!Utils.equal(a[i], b[i],false,precision)) return false;
                }
                return true;
            case BaseType.COLOR:
                if(!precision) {
                    return a.r === b.r && a.g === b.g && a.b === b.b;
                }
                return Utils.zoneEqual(a.r ,precision, b.r )&&
                    Utils.zoneEqual(a.g ,precision, b.g )&&
                    Utils.zoneEqual(a.b ,precision, b.b);
            case BaseType.QUATERNION:
                if(!precision) {
                    return a.x === b.x && a.y === b.y && a.z === b.z&&a.w===b.w;
                }
                return Utils.zoneEqual(a.x ,precision, b.x) &&
                    Utils.zoneEqual(a.y ,precision, b.y) &&
                    Utils.zoneEqual(a.z ,precision, b.z)&&
                    Utils.zoneEqual(a.w,precision,b.w);
            case BaseType.EULER:
                if(!precision) {
                    return a.x === b.x && a.y === b.y && a.z === b.z&&a.order===b.order;
                }
                return Utils.zoneEqual(a.x ,precision, b.x) &&
                    Utils.zoneEqual(a.y ,precision, b.y) &&
                    Utils.zoneEqual(a.z ,precision, b.z)&&
                    a.order===b.order;
            case BaseType.OBJECT:
                let avs = Object.keys(a);
                let bvs = Object.keys(b);
                if (avs.length !== bvs.length) return false;
                for (let ak of avs) {
                    if (bvs.findIndex((bk) => bk === ak) < 0) return false;
                }
                for (let key in a) {
                    if (!Utils.equal(a['' + key + ''], b['' + key + ''])) return false;
                }
                return true;
            default:
                if (Utils.typeOf(a.equals) === BaseType.FUNCTION) {
                    return a.equals(b);
                } else {
                    return a === b;
                }
        }
    }

    /**
     * Get the scene where the 3D object is located
     */
    public static getParentScene(obj:Object3D):Scene|null{
        let _obj=obj;
        while (_obj.parent){
            _obj=_obj.parent;
            if(_obj instanceof Scene)return _obj;
        }
        return null;
    }

    /**
     * Get the highest parent of a 3D object (offset down)
     */
    public static getTopParent(obj:Object3D,offset:number=1):Object3D{
        let _obj=obj;
        const ps=[obj];
        while (_obj.parent){
            ps.push(_obj.parent);
            _obj=_obj.parent;
        }
        const tar=ps[ps.length-1-offset];
        return tar;
    }

    /**
     * Get scene objects according to uuid
     */
    public static getObjectByUuid(scene:Scene,uuid:string):Object3D|null {
        let target=null;
        let found=false;
        scene.traverse((o) => {
            if(!found){
                if (o.uuid !== undefined && o.uuid === uuid) {
                    target= o;
                    found=true;
                }
            }
        });
        return target;
    }

    /**
     * Filter the extra spaces in the string 
     * (filter the first and last spaces, and filter multiple consecutive spaces into one)
     */
    public static filterSpace(text:string):string{
        const V_ARRAY=[];
        let count=0;
        for(let s of text){
            if(s===' '){
                if(count===0){
                    V_ARRAY.push(s);
                }
                count++;
            }else{
                V_ARRAY.push(s);
                count=0;
            }
        }
        if(V_ARRAY[0]===' ')V_ARRAY.splice(0,1);
        if(V_ARRAY[V_ARRAY.length-1]===' ')V_ARRAY.splice(V_ARRAY.length-1,1);
        return V_ARRAY.join('');
    }

    /**
     * Initialize an animated object that implements the setPrg (t) method and other information
     */
    public static initAnimator(actor:Object3D, clip:AnimationClip, loop:AnimationActionLoopStyles=LoopRepeat):AnimateObject {
        const amt:Loose<any> = {};
        amt.uuid=MathUtils.generateUUID();
        amt.root = actor;
        amt.mixer = new AnimationMixer(amt.root);
        amt.action = amt.mixer.clipAction(clip);
        amt.length = amt.action.getClip().duration;
        amt.action.loop = loop;
        amt.prg=0;
        amt.action.play();
        amt.setPrg = (prg:number) => {
            amt.prg =Utils.clamp(prg,0,1);
            amt.action.time = amt.prg * amt.length;
            amt.mixer.update(0);
        };
        return amt as AnimateObject;
    }

    /**
     * Initialize an audio control object
     */
    public static initAudio(listener:AudioListener,buffer:AudioBuffer,isLoop:boolean=false):AudioObject{
        const audio:Loose<any>={};
        audio.content = new Audio(listener);
        audio.content.setBuffer(buffer);
        audio.content.setLoop(isLoop);
        audio.mute=false;
        audio.volume=1;
        audio.active=(bool:boolean)=>{
            audio.mute=!bool;
            if(audio.content.isPlaying){
                audio.content.setVolume(bool?audio.volume:0);
            }
        };
        audio.play=(val:number)=>{
            audio.volume=val;
            let vol=audio.mute?0:val;
            audio.content.setVolume(vol);
            if(!audio.content.isPlaying)audio.content.play();
        };
        audio.stop=()=>{
            if(audio.content.isPlaying)audio.content.stop();
        };
        return audio as AudioObject;
    }

    /**
     * Whether the element is in the element sequence
     */
    public static inElements(e:any, es:Array<any>,compareFunc:CompareFunction<any>=COMPARE_EQUAL_DEFAULT, returnIndex:boolean=false):boolean|number {
        for (let i = 0; i < es.length; i++) {
            if (compareFunc(e, es[i])) {
                return (returnIndex ? i : true);
            }
        }
        return (returnIndex ? -1 : false);
    }

    /**
     * Whether it is a calculable value
     */
    public static isComputableNumber(v:any):boolean{
        if(typeof v!=='number')return false;
        if(Number.isNaN(v))return false;
        return Number.isFinite(v);
    }

    /**
     * PC or mobile
     * @returns {boolean}
     */
    public static isPC():boolean{
        const agent = navigator.userAgent;
        const agents = ['Android', 'Windows Phone', 'iPhone', 'iPod', 'iPad', 'SymbianOS'];
        let isPc = true;
        for (let ag of agents) {
            if (agent.indexOf(ag) > 0) {
                isPc = false;
                break;
            }
        }
        return isPc;
    }

    /**
     * Execute at intervals (by time)
     */
    public static intervalExecuteTime(timeObject:Timer,timeInterval:number,func_interval_return_isExit:IntervalHandle):void{
        Utils.cancelExecuteDelay(timeObject);
        timeObject.time=performance.now();
        let ani=()=>{
            const now=performance.now();
            let exit=false;
            const dta=now-timeObject.time!;
            if(dta>=timeInterval){
                exit=func_interval_return_isExit(dta);
                timeObject.time=now;
            }
            if(exit){
                Utils.cancelExecuteDelay(timeObject);
                return;
            }
            timeObject.id=requestAnimationFrame(ani);
        };
        ani();
    }

    /**
     * Execute at intervals (by frame)
     */
    public static intervalExecuteFrame(frameObject:Timer,frameInterval:number,func_interval_return_isExit:IntervalHandle):void{
        Utils.cancelExecuteDelay(frameObject);
        frameObject.time=0;
        frameObject.now=0;
        let ani=()=>{
            frameObject.now!++;
            let exit=false;
            let dta=frameObject.now!-frameObject.time!;
            if(dta>=frameInterval){
                exit=func_interval_return_isExit(dta);
                frameObject.time=frameObject.now;
            }
            if(exit){
                Utils.cancelExecuteDelay(frameObject);
                return;
            }
            frameObject.id=requestAnimationFrame(ani);
        };
        ani();
    }

    /**
     * whether it is a generalized array (ordinary array and typed array)
     */
    public static isGeneralArray(obj:any):boolean{
        if(typeof obj !== 'object')return false;
        if(Array.isArray(obj))return true;
        return obj[0]!==undefined;
    }

    /**
     * Loop recursive exit/interrupt
     */
    public static loopSafe(count:number,limit:number=1000,call:{():void}|null=null):number{
        if(count>limit){
            if(!call) throw new Error('recursion/loop did not break out by given times!');
            call();
        }
        return count+1;
    }

    /**
     * Reciprocating interpolation
     */
    public static pingPong(start:number,end:number,t:number):number{
        let _t=t-Math.floor(t);
        if(_t<=0.5){
            return (end-start)*_t*2+start;
        }else{
            return (end-start)*(1-_t)*2+start;
        }
    }

    /**
     * whether the string is a number
     */
    public static stringIsNumber(str:string):boolean {
        if(str.charAt(0)==='.'||str.charAt(str.length-1)==='.')return false;
        let reg = /[0-9]/g;
        let tp=str;
        tp=tp.replace(reg,'');
        return (tp===''||tp==='.');
    }

    /**
     * Generate cabinet projection matrix
     */
    public static makeCabinetShearMatrix(angle:number,offsetX:number,offsetY:number,offsetZ:number):Matrix4 {
        const mtx=new Matrix4();
        const te=mtx.elements;
        te[ 0 ] = 1;	    te[ 4 ] = 0;	    te[ 8 ] = -0.5 * Math.cos(angle);	    te[ 12 ] = offsetX;
        te[ 1 ] = 0;	    te[ 5 ] = 1;	    te[ 9 ] = -0.5 * Math.sin(angle);	    te[ 13 ] = offsetY;
        te[ 2 ] = 0;	    te[ 6 ] = 0;	    te[ 10 ] = 1;	                        te[ 14 ] = offsetZ;
        te[ 3 ] = 0;	    te[ 7 ] = 0;	    te[ 11 ] = 0;	                        te[ 15 ] = 1;
        return mtx;
    }

    /**
     * Correct number according to accuracy
     * 152.42645,0.05->152.45
     */
    public static normalizeNumber(number:number,normal:number=0.02):string{
        const dec=normal;
        const decN=dec;
        const temp=dec.toString();
        const l=temp.length;
        let dl=temp.indexOf(".");
        dl=dl===-1?0:dl;
        const fl=dl===0?0:l-dl-1;
        const count=Math.round(number/decN)*decN;
        return (count).toFixed(fl);
    }

    /**
     * Get Adjacent Value (Index)
     * [0,1,2,3,4,5,6,7]
     * dv:2,min:1,max:6,offset:5->1
     */
    public static neighborValue(dv:number,min:number,max:number,offset:number=1):number{
        const range=max-min+1;
        let nv=dv+offset;
        do{
            nv+=range;
        }while (nv<0);
        nv%=range;
        return min+nv;
    };

    /**
     * Play in sequence (animated objects that implement. setPrg (t)) or function execution
     */
    public static playSequence(requests:Array<number|null>, requestIndex:number, ...sequence_ani_s_e_sp_sCall_pCall_eCall:Array<SequenceStep>):void {
        if(requests[requestIndex]!==null){
            cancelAnimationFrame(requests[requestIndex]!);
            requests[requestIndex] = null;
        }

        let prg = 0;
        let length = sequence_ani_s_e_sp_sCall_pCall_eCall.length;
        let index = 0;
        while (sequence_ani_s_e_sp_sCall_pCall_eCall[index] === null) {
            index++;
            if (index === length) break;
        }
        if (index === length) {
            return;
        }
        let animator = sequence_ani_s_e_sp_sCall_pCall_eCall[index][0];
        let start = sequence_ani_s_e_sp_sCall_pCall_eCall[index][1];
        let end = sequence_ani_s_e_sp_sCall_pCall_eCall[index][2];
        let delta = end - start;
        let speed = sequence_ani_s_e_sp_sCall_pCall_eCall[index][3];
        let startCall = sequence_ani_s_e_sp_sCall_pCall_eCall[index][4];
        let progressCall = sequence_ani_s_e_sp_sCall_pCall_eCall[index][5];
        let endCall = sequence_ani_s_e_sp_sCall_pCall_eCall[index][6];
        let spdR=1;
        if (startCall) spdR=startCall();
        spdR=spdR?spdR:1;
        let ani = () => {
            prg += speed*spdR;
            prg = prg < 0 ? 0 : (prg > 1 ? 1 : prg);
            let t = delta * prg + start;
            if (animator) animator.setPrg(t);
            if (progressCall) progressCall(prg);
            if (prg === 1) {
                prg = 0;
                if (endCall) endCall();
                index++;
                while (sequence_ani_s_e_sp_sCall_pCall_eCall[index] === null) {
                    index++;
                    if (index === length) break;
                }
                if (index === length) {
                    cancelAnimationFrame(requests[requestIndex]!);
                    requests[requestIndex] = null;
                    return;
                }
                animator = sequence_ani_s_e_sp_sCall_pCall_eCall[index][0];
                start = sequence_ani_s_e_sp_sCall_pCall_eCall[index][1];
                end = sequence_ani_s_e_sp_sCall_pCall_eCall[index][2];
                delta = end - start;
                speed = sequence_ani_s_e_sp_sCall_pCall_eCall[index][3];
                startCall = sequence_ani_s_e_sp_sCall_pCall_eCall[index][4];
                progressCall = sequence_ani_s_e_sp_sCall_pCall_eCall[index][5];
                endCall = sequence_ani_s_e_sp_sCall_pCall_eCall[index][6];
                if (startCall) spdR=startCall();
                spdR=spdR?spdR:1;
            }
            requests[requestIndex] = requestAnimationFrame(ani);
        };
        ani();
    }

    /**
     * quick sort
     */
    public static quickSort(ary:ArrayLike<any>, compareFunc:CompareFunction<any>=COMPARE_VALUE_DECREASE):void {
        if (ary.length > 1) {
            SORT_ARRAY(ary as Array<any>, compareFunc, 0, ary.length - 1);
        }
    }

    /**
     * Reset typed array length
     */
    public static resizeTypedArray(ary:TypedArray, len:number):TypedArray {
        let nary;
        switch ((ary as any).__proto__) {
            case Int8Array.prototype:
                nary = new Int8Array(len);
                break;
            case Uint8Array.prototype:
                nary = new Uint8Array(len);
                break;
            case Int16Array.prototype:
                nary = new Int16Array(len);
                break;
            case Uint16Array.prototype:
                nary = new Uint16Array(len);
                break;
            case Int32Array.prototype:
                nary = new Int32Array(len);
                break;
            case Uint32Array.prototype:
                nary = new Uint32Array(len);
                break;
            case Float32Array.prototype:
                nary = new Float32Array(len);
                break;
            default:
                console.warn('unsupported type!');
                nary = new Float32Array(len);
                break;
        }
        for (let i = 0; i < ary.length; i++) {
            if (nary[i] !== undefined) {
                nary[i] = ary[i];
            } else {
                break;
            }
        }
        return nary;
    }

    /**
     * Remove duplicate elements from a array
     */
    public static removeDuplicated(ary:Array<any>, compareFunc:CompareFunction<any>=COMPARE_EQUAL_DEFAULT):void {
        const temp=[...ary];
        ary.length=0;
        for(let t of temp){
            if(!Utils.inElements(t,ary,compareFunc))ary.push(t);
        }
    }

    /**
     * Random number of specific interval
     */
    public static random(min:number, max:number):number {
        let delta = max - min;
        let rDelta = delta * Math.random();
        return min + rDelta;
    }

    /**
     * Initialize a coroutine object
     */
    public static requestAnimationFrame(func_t_return_break:{(t:number):boolean},min:number=20,max:number=200,timerObject:Timer|null=null):Timer{
        const timer=timerObject||{};
        if(timer.hasOwnProperty('id')){
            Utils.cancelAnimationFrame(timer);
        }
        timer.info=new Float32Array([min,max,0,0,0]);
        timer.func=func_t_return_break;
        let ani=()=>{
            let info=timer.info;
            info[2]=performance.now();
            let d=info[2]-info[3];
            if(d>info[0]){
                info[3]=d>info[0]*2?info[2]:info[3]+info[0];
                d=info[2]-info[4];
                info[4]=info[2];
                if(timer.func!(d>info[1]?info[1]:d)){
                    cancelAnimationFrame(timer.id!);
                    return;
                }
            }
            timer.id=requestAnimationFrame(ani);
        };
        ani();
        return timer;
    }

    /**
     * Convert screen coordinates to world coordinates
     */
    public static screenPointToWorld(rect:BaseRect, camera:Camera, focus:number|Vector3, point:Vector2, isFlat:boolean = true, scale:number = 1, isRatio:boolean = false,out:Vector3|null=null):Vector3|boolean {
        let x=0, y=0;
        if (!isRatio) {
            x = point.x - rect.left;
            y = point.y - rect.top;
        }
        const rx = isRatio ? point.x : (x / rect.width);
        const ry = isRatio ? point.y : (y / rect.height);
        tV0.x = (rx * 2 - 1) * scale;
        tV0.y = (1 - ry * 2) * scale;
        const raycaster = tR0;
        camera.updateMatrixWorld();
        (camera as any).updateProjectionMatrix();
        raycaster.setFromCamera(tV0, camera);
        const plane = tP0;
        const dir = tV1;
        tM0.identity();
        isFlat?
            camera.getWorldDirection(dir):
            dir.copy(raycaster.ray.direction);
        if(typeof focus==='number'){
            tV2.copy(dir).multiplyScalar(focus);
            tV3.copy(raycaster.ray.origin);
            tV3.add(tV2);
            plane.setFromNormalAndCoplanarPoint(dir.negate(),tV3);
        }else{
            plane.setFromNormalAndCoplanarPoint(dir.negate(),focus);
        }
        const intersect=out||new Vector3();
        raycaster.ray.intersectPlane(plane,intersect);
        return (intersect) ? intersect : false;
    }

    /**
     * Get the serial number sorted by serial value
     */
    public static sortIndexArray(ary:Array<any>,compareFunc:CompareFunction<any>=COMPARE_VALUE_DECREASE):Uint16Array{
        const iAry=new Uint16Array(ary.length);
        for(let i=0;i<iAry.length;i++){
            iAry[i]=i;
        }
        const compare=(a:any,b:any)=>{
            return compareFunc(ary[a],ary[b]);
        };
        Utils.quickSort(iAry,compare);
        return iAry;
    }

    /**
     * Set up ray testing
     */
    public static setCaster(camera:Camera,rect:BaseRect,event:Vector2|CrossEventObject,caster:Raycaster,pointerOut:Vector2=new Vector2(),pointerVecOut:Vector2=new Vector2()):void{
        if(event){
            if(event.isVector2){//20220417
                pointerOut.copy(event as Vector2);
            }else{
                const evnt=event as CrossEventObject;
                evnt.isMouse ?
                    pointerOut.set(evnt.clientX!, evnt.clientY!) :
                    pointerOut.set(evnt.touches![0].clientX, evnt.touches![0].clientY);
            }
        }
        if(pointerVecOut) {
            const rc = rect;
            let x = (pointerOut.x - rc.left) / rc.width;
            let y = (pointerOut.y - rc.top) / rc.height;
            pointerVecOut.set((x * 2) - 1, 1 - (y * 2));
        }
        if(caster&&pointerVecOut)caster.setFromCamera(pointerVecOut, camera);
    }

    /**
     * Select and change the original array
     */
    public static select<T>(pool:Array<T>,mode:'random'|'begin'|'end'='random'):T|null{
        let re=null;
        if(pool.length===0)return re;
        switch (mode) {
            case 'random':
                const i=Math.round(Utils.random(0,pool.length-1));
                re=pool[i];
                pool.splice(i,1);
                break;
            case 'begin':
                re=pool[0];
                pool.splice(0,1);
                break;
            case 'end':
                re=pool[pool.length-1];
                pool.splice(pool.length-1,1);
                break;
        }
        return re;
    }

    /**
     * Get (common type or any object that implements the. type attribute) object type 
     * (use explicit method when frequent detection is required)
     */
    public static typeOf(object:any):string {
        if (object === null) return BaseType.NULL;
        if (object === undefined) return BaseType.UNDEFINED;
        if (typeof object === 'number') return BaseType.NUMBER;
        if (typeof object === 'string') return BaseType.STRING;
        if (typeof object === 'boolean') return BaseType.BOOLEAN;
        if (typeof object === 'symbol') return BaseType.SYMBOL;
        if (Array.isArray(object)) return BaseType.ARRAY;
        if (object.isVector3) return BaseType.VECTOR3;
        if (object.isVector4) return BaseType.VECTOR4;
        if (object.isVector2) return BaseType.VECTOR2;
        if (object.isMatrix3) return BaseType.MATRIX33;
        if (object.isMatrix4) return BaseType.MATRIX44;
        if (object.isColor) return BaseType.COLOR;
        if (object instanceof Quaternion) return BaseType.QUATERNION;
        if (object.isEuler) return BaseType.EULER;
        if (typeof object === 'function') return BaseType.FUNCTION;
        if (object.type !== undefined) return object.type;
        return BaseType.OBJECT;
    }

    /**
     * Construct Y-up vector from Z-up vector parameters
     */
    public static vectorY(x:number,y:number,z:number):Vector3{
        return new Vector3(x,z,-y);
    }

    /**
     * Linear cross reference interpolation (node positions correspond to monotonic changes of 'from' and 'to')
     * from:0,to:1,[0,2, 0.5,8, 1,3]
     * t:0.75->return 5.5
     */
    public static valueLinearMapping(t:number,from:number,to:number,node_value_ary:Array<number>):number{
        let ascending=true;
        if(node_value_ary[0]===from&&node_value_ary[node_value_ary.length-2]===to){
            ascending=from<to;
        }else{
            console.warn('[mapValue] need more aligned data!');
            return t;
        }
        const v=Utils.clamp(t,from,to);
        for(let i=0;i<node_value_ary.length;i+=2){
            const node=node_value_ary[i];
            if(node===v){
                return node_value_ary[i+1];
            }
            let n=node_value_ary[i+2],s=0,e=0;
            const sus=ascending?(v>node&&v<n):(v<node&&v>n);
            if(sus){
                s=node_value_ary[i+1];
                e=node_value_ary[i+3];
                return s+(e-s)*(v-node)/(n-node);
            }
        }
        console.warn('[mapValue] mapping failed!');
        return t;
    }

    /**
     * cancel 'waitToExecute' immediately
     */
    public static cancelExecuteDelay(delayObject:Timer):void{
        if(delayObject.id===null)return;
        cancelAnimationFrame(delayObject.id!);
        delayObject.id = null;
    }

    /**
     * execute only once (by frame) within the specified interval
     */
    public static waitToExecuteFrame(frameObject:Timer, frameCount:number, fuc:{():void},isHead:boolean=true):void {
        if(isHead) {
            if (frameObject.id !== null && frameObject.id !== undefined) {
                return;
            }
            frameObject.counter = 0;
            const ani = () => {
                frameObject.counter!++;
                if (frameObject.counter! > frameCount) {
                    cancelAnimationFrame(frameObject.id!);
                    frameObject.id = null;
                    fuc();
                    return;
                }
                frameObject.id = requestAnimationFrame(ani);
            };
            ani();
        }else{
            if(!frameObject.counter)frameObject.counter=0;
            if (frameObject.id !== null && frameObject.id !== undefined) {
                cancelAnimationFrame(frameObject.id);
                frameObject.id = null;
            }
            const ani = () => {
                frameObject.counter!++;
                if (frameObject.counter! > frameCount) {
                    cancelAnimationFrame(frameObject.id!);
                    frameObject.id = null;
                    frameObject.counter=0;
                    fuc();
                    return;
                }
                frameObject.id = requestAnimationFrame(ani);
            };
            ani();
        }
    }

    /**
     * execute only once (by time) within the specified interval
     */
    public static waitToExecuteTime(timeObject:Timer, timeInterval:number, fuc:{():void},isHead:boolean=true):void {
        if(isHead) {
            if (timeObject.id !== null && timeObject.id !== undefined) {
                return;
            }
            timeObject.counter = performance.now() + timeInterval;
            const ani = () => {
                if (timeObject.counter! < performance.now()) {
                    cancelAnimationFrame(timeObject.id!);
                    timeObject.id = null;
                    fuc();
                    return;
                }
                timeObject.id = requestAnimationFrame(ani);
            };
            ani();
        }else{
            if(!timeObject.counter)timeObject.counter=performance.now()+timeInterval;
            if (timeObject.id !== null && timeObject.id !== undefined) {
                cancelAnimationFrame(timeObject.id);
                timeObject.id = null;
            }
            let ani = () => {
                if (timeObject.counter! < performance.now()) {
                    cancelAnimationFrame(timeObject.id!);
                    timeObject.id = null;
                    timeObject.counter=0;
                    fuc();
                    return;
                }
                timeObject.id = requestAnimationFrame(ani);
            };
            ani();
        }
    }

    /**
     * Convert world coordinates to screen coordinates
     */
    public static worldPointToScreen(camera:Camera, rect:BaseRect, point:Vector3, outPoint:Vector3|null = null):Vector3 {
        let halfWidth = rect.width / 2;
        let halfHeight = rect.height / 2;
        camera.updateMatrixWorld();
        (camera as any).updateProjectionMatrix();
        let tempPoint = outPoint || new Vector3();
        tempPoint.copy(point).project(camera);
        tempPoint.x = (tempPoint.x * halfWidth) + halfWidth;
        tempPoint.y = -(tempPoint.y * halfHeight) + halfHeight;
        tempPoint.z = 0;
        return tempPoint;
    }

    /**
     * Whether the two values are in the same range
     */
    public static zoneEqual(target:number, delta:number, value:number):boolean {
        return (value > target - delta && value < target + delta);
    }

    /**
     * whether the two ranges are overlap
     */
    public static zoneOverlap(min0:number,max0:number,min1:number,max1:number,threshold:number=MIN_FLOAT):boolean{
        if((min0>=(min1-threshold))&&(min0<=(max1+threshold)))return true;
        if((max0>=(min1-threshold))&&(max0<=(max1+threshold)))return true;
        if((min1>=(min0-threshold))&&(min1<=(max0+threshold)))return true;
        return (max1>=(min0-threshold))&&(max1<=(max0+threshold));
    }
}

export {
    Utils,
    BaseType
};
export type{
    CompareFunction,
    Loose,
    AssetModel,
    Timer,
    AnimateObject,
    AudioObject,
    SequenceStep,
    TypedArray,
    BaseRect,
    ArrayLike
};